package org.uc3m.pfc.speech;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import org.opencv.android.CameraBridgeViewBase;
import org.uc3m.pfc.lifecycle.LifeCycle;
import org.uc3m.pfc.lifecycle.State;
import org.uc3m.pfc.ui.GuiUtils;

import static org.uc3m.pfc.lifecycle.State.*;

import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.speech.tts.TextToSpeech;
import android.util.Log;

/**
 * 
 * @author esteban
 *
 */
public final class SpeechManager extends LifeCycle implements ISpeechManager, 
	RecognitionListener, TextToSpeech.OnInitListener, 
	TextToSpeech.OnUtteranceCompletedListener {

	// Class fields *******************************************************
	
	/** Debug flag for enable/disable voice. */
	private static final boolean DISABLE_VOICE_SPEECH = false;

	/** Constant for max voice results. */
	private static final String MAX_VOICE_RESULTS = "5";

	/** Timeout for voice recog. */
	private static final long TIME_OUT_FOR_VOICE_RECOG = 5000;

	/** Static instance. */
	private static ISpeechManager instance;
	

	/** TTS instance. */
	private TextToSpeech tts;

	/** Intent for voice recognition. */
	private Intent intent;
	/** Speech recognizer object. */
	private SpeechRecognizer sr;

	/** Audio manager for mute the starting of voice recognition. */
	private AudioManager amanager;

	/** Flag to know if speech recog is alive. */
	private boolean isSpeechRecognizerAlive;

	/** List of words to match. */
	private List<String> words;

	/** Current word to use. */
	private String word;

	/** Context for getting speech resources. */
	private Context context;
	
	/** Object for synchronization. */
	private Object syncToken = new Object();	
	
	/** Utility for UI interactions. */
	private GuiUtils guiUtils;

	
	// Constructor *******************************************************
	
	/**
	 * Private default constructor.
	 */
	private SpeechManager() {

		guiUtils = GuiUtils.getInstance();
		
		this.context =  guiUtils.getContext();
		start();
		
		setState(stopped);
	}
	
	/**
	 * Start the manager.
	 */
	private void start() {

		// create the TTS object
		tts = new TextToSpeech(context, this);

		// create the speech recognizer object
		sr = SpeechRecognizer.createSpeechRecognizer(context);
		sr.setRecognitionListener(this);

		// create the intent for speech recognizer
		intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);

		intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
				RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
		intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, this.getClass()
				.getPackage().getName());

		intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, MAX_VOICE_RESULTS);

		// get the audio manager, disable beep sound of recognizer
		amanager = (AudioManager) context.getSystemService(
				Context.AUDIO_SERVICE);
	
	}

	
	/**
	 * Get the instance.
	 * 
	 * @return instance
	 */
	public static ISpeechManager getInstance() {
		
		if (instance == null) {
			instance = new SpeechManager(); 
		}
		
		return instance;
	}

	
	// ISpeechManager methods **********************************************

	@Override
	public void speak(final String message) {

		setState(speacking);
		setSubstate(speacking);
		
		speakOut(message, SpeechType.speakAndReturn);
		
		setState(stopped);
		setSubstate(stopped);
	}
	
	@Override
	public String recognize(final List<String> pwords) {
		
		this.words = pwords;
		
		setState(recognizing);
		setSubstate(recognizing);
		
		recognize();
		
		if (this.getSubstate() != stopping) {
			waitForRecog();
		}
		
		setState(stopped);
		setSubstate(stopped);
		
		return getWord();
	}	
	
	@Override
	public String speechAndRecognize(final String message,
			final List<String> pwords) {

		this.words = pwords;

		setState(speacking);
		setSubstate(speacking);
		
		speakOut(message, SpeechType.speakAndRecog);

		if (this.getSubstate() != stopping) {
		
			setState(recognizing);
			setSubstate(recognizing);
			
			recognize();
			
			if (this.getSubstate() != stopping) {
				waitForRecog();
			}
		
		}
		
		setState(stopped);
		setSubstate(stopped);
		
		return getWord();
	}

	@Override
	public void stopAll() {

		this.setSubstate(stopping);


		if (this.getState() == speacking) {
			
			if (tts != null) {
				
				if (tts.isSpeaking()) {
					tts.stop();
					
					notifyFromSpeech();
				}
			}
		}

		if (this.getState() == recognizing) {
			
			if (sr != null) {
				
				sr.stopListening();
				sr.cancel();
				
				setWord(null);
				
				notifyFromRecog();
			}
		}
	}
	
	@Override
	public void stop() {

		if (tts != null) {
			tts.stop();
			tts.shutdown();
		}

	}

	
	// LifeCycle methods **********************************************
		
	@Override
	public void setState(final State pstate) {
	
		super.setState(pstate);
		
		switch (pstate) {
		case speacking:
			
			CameraBridgeViewBase.enableVoiceSpeak = true;
			CameraBridgeViewBase.enableVoiceRecog = false;			
			
			break;
		case recognizing:
			
			CameraBridgeViewBase.enableVoiceSpeak = false;
			CameraBridgeViewBase.enableVoiceRecog = true;			
			
			break;			
		case stopped:
			
			CameraBridgeViewBase.enableVoiceSpeak = false;
			CameraBridgeViewBase.enableVoiceRecog = false;
			
			break;			

		default:

			CameraBridgeViewBase.enableVoiceSpeak = false;
			CameraBridgeViewBase.enableVoiceRecog = false;
			break;
		}
		
	}
	
	
	// Private methods ***************************************************
	
	/**
	 * Speak out the given text.
	 * 
	 * @param text to speak
	 * @param speechType type of speech
	 */
	private void speakOut(final String text, final SpeechType speechType) {

		HashMap<String, String> myHashAlarm = new HashMap<String, String>();
		myHashAlarm.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
				String.valueOf(AudioManager.STREAM_ALARM));
		myHashAlarm.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
				speechType.name());

		String message = text;
		if (DISABLE_VOICE_SPEECH) {
			message = "";
		}

		int error = tts.speak(message, TextToSpeech.QUEUE_FLUSH, myHashAlarm);

		if (error != 0) {
			// TODO manage error
			Log.i("Error", "error code speaking: " + error);
		}

		if (getSubstate() != stopping) {

			waitForSpeech();
		}

	}

	/**
	 * Speak is finished, notify.
	 * 
	 * @param id of speech
	 */
	private void onFinishSpeech(final String id) {

		if (DISABLE_VOICE_SPEECH) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
		
				e.printStackTrace();
			}
		}
		
		notifyFromSpeech();
	}

	/**
	 * Start speech recognition process.
	 */
	private void recognize() {
		
		final Runnable speechStart = new Runnable() {
			public void run() {

				Log.d("Speech", "finished");
				Log.d("Recog", "starting");
				try {
					amanager.setStreamMute(AudioManager.STREAM_SYSTEM, 
							true);
					sr.startListening(intent);

				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		};

		guiUtils.invokeUiThread(speechStart);		
		
	}
	
	
	// TextToSpeech.OnInitListener methods ********************************
	
	@Override
	public void onInit(final int status) {

		if (status == TextToSpeech.SUCCESS) {

			int result = tts.setLanguage(Locale.getDefault());

			if (result == TextToSpeech.LANG_MISSING_DATA
					|| result == TextToSpeech.LANG_NOT_SUPPORTED) {
				Log.e("TTS", "This Language is not supported");
			} else {
				tts.setOnUtteranceCompletedListener(this);
			}

		} else {
			Log.e("TTS", "Initilization Failed!");
		}

	}

	// TextToSpeech.OnUtteranceCompletedListener methods *******************
	
	@Override
	public void onUtteranceCompleted(final String utteranceId) {

		onFinishSpeech(utteranceId);
	}
	

	// RecognitionListener methods *****************************************
	
	@Override
	public void onReadyForSpeech(final Bundle params) {

		setWord(null);
		isSpeechRecognizerAlive = false;
		mNoSpeechCountDown.start();

		amanager.setStreamMute(AudioManager.STREAM_SYSTEM, false);

	}

	@Override
	public void onBeginningOfSpeech() {
		isSpeechRecognizerAlive = true;
		Log.d("Recog", "onBeginningOfSpeech");
	}

	@Override
	public void onRmsChanged(final float rmsdB) {
	}
	
	@Override
	public void onBufferReceived(final byte[] buffer) {
	}
	
	@Override
	public void onEndOfSpeech() {
	}

	@Override
	public void onError(final int error) {
		Log.i("Error", "error code: " + error);
		setWord(null);
		
		notifyFromRecog();
	}

	@Override
	public void onResults(final Bundle results) {

		String str = new String();
		Log.d("Recog", "onResults");
		ArrayList<String> data = results
				.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);

		boolean found = false;
		for (int i = 0; i < data.size(); i++) {
			str = data.get(i).toLowerCase();
			Log.d("Recog", str);
			if (words.contains(str)) {
				found = true;
				break;
			}
		}

		if (found) {
			Log.d("Recog", "found word!");
			setWord(str);
		} else {
			setWord(null);
		}
		
		notifyFromRecog();
	}

	@Override
	public void onPartialResults(final Bundle partialResults) {
		Log.d("Recog", "onPartialResults");
	}

	@Override
	public void onEvent(final int eventType, final Bundle params) {
		Log.d("Recog", "onEvent " + eventType);
	}

	
	// Private auxiliary methods *******************************************
	
	/** Count down timer for Jelly Bean work around. */
	private CountDownTimer mNoSpeechCountDown = 
			new CountDownTimer(TIME_OUT_FOR_VOICE_RECOG, 
					TIME_OUT_FOR_VOICE_RECOG) {

		@Override
		public void onTick(final long millisUntilFinished) {
		}

		@Override
		public void onFinish() {

			if (!isSpeechRecognizerAlive) {

				Log.i("Recog", "not alive, stopping");

				if (sr != null) {
					sr.stopListening();
					sr.cancel();
					
					setWord(null);
					
					notifyFromRecog();
				}
			}

		}

	};

	/**
	 * Set the recognized word.
	 * 
	 * @param pword recognized word
	 */
	private void setWord(final String pword) {
		word = pword;
	}

	/**
	 * Get the recognized word.
	 * 
	 * @return word
	 */
	private String getWord() {
		return word;
	}

	/**
	 * Wait until speech is finished.
	 */
	private void waitForSpeech() {

		Log.d("Speech", "wait");
		

		waitProcess();		
		
	}
	
	/**
	 * Wait until voice recognition is finished.
	 */
	private void waitForRecog() {

		Log.d("Recog", "wait");
		
		waitProcess();
	}

	/**
	 * Wait for process.
	 */
	private void waitProcess() {
		
        synchronized (syncToken) {
            try {
                syncToken.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }		
	}
	
	/**
	 * Notify speech finished.
	 */
	private void notifyFromSpeech() {
		
		Log.d("Speech", "notify");
		
		notifyProcess();
	}
	
	/**
	 * Notify voice recognition finished.
	 */
	private void notifyFromRecog() {

		
		Log.d("Recog", "notify");
		
		notifyProcess();
		
	}

	/**
	 * Notify process.
	 */
	private void notifyProcess() {

        synchronized (syncToken) {
            syncToken.notify();
        }
	}
	
}
